package sg.edu.nus.iss.billing.datastore;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import sg.edu.nus.iss.billing.exception.DataStoreException;

/**
 * Abstract class for all file storage used in data store 
 * @author Wang Dayong
 *
 */
public abstract class AbstractFileStorage {

	private static String PATH_SEP = System.getProperty("file.separator");

	// cache is global cache, and default cache is on
	private static Map<String, byte[]> cache = new HashMap<String, byte[]>();

	private String rootPath;

	// default value: cache is on
	private boolean isCacheOn = true;

	// indicator for write to file instantly, default is true
	private boolean isInstantFlush = true;

	/**
	 * Constructor 
	 * @param rootPath the storage root path
	 * @param isCacheOn whether enable cache, the cache will sync with file on HD
	 */
	public AbstractFileStorage(String rootPath, boolean isCacheOn) {

		super();
		this.rootPath = rootPath;
		this.isCacheOn = isCacheOn;
	}

	/**
	 * 
	 * @param rootPath the storage root path
	 * 	This constructor set cache on as default
	 */
	public AbstractFileStorage(String rootPath) {

		this(rootPath, true);
	}

	/**
	 * save object to specified file name under root folder, if cache
	 * is on and flush immediately is on, will write to file and cache, if cache
	 * is off will write to file directly, else will only write to cache, need to
	 * invoke flush to save changes
	 * 
	 * @param fileName
	 *            file name under root folder
	 * @param obj
	 *            objects need to save
	 * @throws DataStoreException
	 */
	public void save(String fileName, Object obj) throws DataStoreException {

		try {
			if (isCacheOn) {
				// get the byte array of objects, and write to cache
				byte[] content = getBytesFromObject(obj);
				cache.put(fileName, content);
				// if instant flush is on, will write to file
				if (isInstantFlush) {
					saveBytes2file(fileName, content);
				}
			} else {
				// if cache is off, write to file directlly
				writeObject2File(fileName, obj);
			}

		} catch (IOException e) {
			throw new DataStoreException(e);
		}

	}

	/**
	 * load object from specified file name under root folder</br> if
	 * cache is on, will find from cache first, if not in cache, will load from file
	 * and put into cache. if cache is off, will load from file directly
	 * 
	 * @param fileName
	 *            file name under root folder
	 * @param skipCache
	 *            indicator whether skip load from cache(can read updated data
	 *            from other session)
	 * @return Object
	 * @throws DataStoreException
	 */
	public Object load(String fileName) throws DataStoreException {

		try {
			// load from cache, if cache is on and contains the content needed
			if (isCacheOn&&cache.containsKey(fileName)) {
				byte[] content = cache.get(fileName);
				return loadObjectFromBytes(content);
			}
			// cache is on, and can not get content from cache, get from file
			// and add to cache
			if (isCacheOn) {
				byte[] content = readBytesFromFile(fileName);
				// if content is null, means no data stored in specified file
				if (content==null) {
					return null;
				}
				// put loaded content to cache
				cache.put(fileName, content);
				// convert from byte array to object
				return loadObjectFromBytes(content);
			} else {
				// if cache is off, load from file
				return loadObjectFromFile(fileName);
			}

		} catch (IOException e) {
			throw new DataStoreException(e);
		} catch (ClassNotFoundException e) {
			throw new DataStoreException(e);
		}

	}

	/**
	 * flush cache to data file, if specify types, will flush specified types,
	 * else will flush all to data file
	 * 
	 * @throws DataStoreException
	 */
	public void flush(String... types) throws DataStoreException {

		if (isCacheOn&&!isInstantFlush) {
			try {
				if (types==null||types.length==0) {
					// TODO:not consider the efficiency, add version check
					// latter
					for (Map.Entry<String, byte[]> entry : cache.entrySet()) {
						this.saveBytes2file(entry.getKey(), entry.getValue());
					}
				} else {
					for (String type : types) {
						this.saveBytes2file(type, cache.get(type));
					}
				}
			} catch (IOException e) {
				throw new DataStoreException(e);
			}
		}

	}

	/**
	 * write a object of object to specified file under root folder
	 * 
	 * @param fileName
	 * @param obj
	 * @throws IOException
	 */
	protected abstract void writeObject2File(String fileName, Object obj) throws IOException;

	/**
	 * load object from specified file name under root folder
	 * 
	 * @param fileName
	 * @return Object
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	protected abstract Object loadObjectFromFile(String fileName) throws IOException, ClassNotFoundException;

	/**
	 * convert byte array to object
	 * 
	 * @param bytes
	 * @return
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	protected abstract Object loadObjectFromBytes(byte[] bytes) throws IOException, ClassNotFoundException;

	/**
	 * convert object to byte array
	 * 
	 * @param saveObj
	 * @return
	 * @throws IOException
	 */
	protected abstract byte[] getBytesFromObject(Object saveObj) throws IOException;

	/**
	 * clear all contents in cache, this will rollback all changes which not
	 * write to data file
	 */
	public void clearCache() {

		if (cache!=null) {
			cache.clear();
		}
	}

	public void configCache(boolean isOn) {

		this.isCacheOn = isOn;

	}

	public void setStorageRootPath(String path) {

		this.rootPath = path;

	}

	protected String getRealPath(String fileName) {

		return rootPath.concat(PATH_SEP).concat(fileName);
	}

	public void setInstantFlush(boolean instantly) {

		this.isInstantFlush = instantly;
	}

	public boolean isInstantFlush() {

		return isInstantFlush;
	}

	public void setRootPath(String rootPath) {

		this.rootPath = rootPath;
	}

	/**
	 * save byte array to specified path
	 * 
	 * @param fileName
	 * @param bytes
	 * @throws IOException
	 */
	private void saveBytes2file(String fileName, byte[] bytes) throws IOException {

		if (bytes==null||bytes.length==0) {
			return;
		}

		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(new File(getRealPath(fileName)));
			fos.write(bytes);
			fos.flush();
		} finally {
			if (fos!=null)
				fos.close();
		}
	}

	/**
	 * read file to byte array, if file not found will return null
	 * 
	 * @param fileName
	 * @return
	 * @throws IOException
	 */
	private byte[] readBytesFromFile(String fileName) throws IOException {

		FileInputStream fis = null;
		try {
			fis = new FileInputStream(getRealPath(fileName));
			byte[] buf = new byte[fis.available()];
			fis.read(buf);
			return buf;
		} catch (FileNotFoundException ex) {
			return null;
		} finally {
			if (fis!=null)
				fis.close();
		}

	}

	/**
	 * copy file from 'orig' to 'copy' under root folder
	 * if cache is on, will execute this operation on the related content in the cache
	 * 
	 * @param orig
	 * @param copy
	 * @throws IOException
	 */
	public void copyFile(String orig, String copy) throws IOException {

		if (isCacheOn) {
			byte[] content = cache.get(orig);
			byte[] newContent = null;
			if (content!=null) {
				newContent = Arrays.copyOf(content, content.length);
			}
			cache.put(copy, newContent);
			// if this content is new one, no need to copy file from HDD
			if (content==null) {
				return;
			}
		}

		// if the original file not exists, return
		File origFile = new File(getRealPath(orig));
		if (!origFile.exists()) {
			return;
		}

		if (isInstantFlush||!isCacheOn) {
			OutputStream os = null;
			InputStream is = null;
			try {

				os = new FileOutputStream(getRealPath(copy));

				is = new FileInputStream(getRealPath(orig));
				byte[] buf = new byte[255];
				int len = 0;
				while ((len = is.read(buf))!=-1) {
					os.write(buf, 0, len);
				}
				os.flush();
			} finally {
				if (is!=null)
					is.close();
				if (os!=null)
					os.close();
			}
		}

	}

	/**
	 * delete specified file, if cache is on will delete the related content in cache
	 * 
	 * @param delete
	 * @throws IOException
	 */
	public void deleteFile(String delete) throws IOException {

		if (isCacheOn) {
			cache.remove(delete);
		}

		if (!isCacheOn||isInstantFlush) {
			File delFile = new File(getRealPath(delete));
			if (delFile.exists()) {
				if (delFile.isDirectory())
					throw new FileNotFoundException("delete file is directory");
				else
					delFile.delete();
			}
		}

	}

}
